Near-Plane Clipping - Sixth 3D

Table of Contents

<- Back to index

1. The problem

When the camera brushes against geometry — a floor tile under your feet, a wall you lean into — part of a polygon can end up behind the viewer while the rest stays in front. Perspective projection divides by depth (screenX = x / z), so a vertex at z ≤ 0 has no meaningful screen position at all.

The naive way out — dropping any polygon that has even one vertex behind the camera — makes whole tiles vanish exactly when they are closest and largest on screen. Walking through the House demo, floor tiles blinked out of existence at the bottom of the frame:

near-clip-before.png

Sixth 3D instead clips the polygon against the near plane and renders the surviving fragment. The same frame with clipping enabled — the floor is solid to the bottom edge:

near-clip-after.png

Think of the camera plane as the edge of a table and the polygon as a sheet of paper partly hanging off it. Dropping the polygon means throwing away the whole sheet. Clipping takes scissors, cuts the sheet along the table edge, and keeps the part that lies on the table.

z (depth) → x ↓ camera near plane z = 1 floor tiles kept fragment cut away old: one vertex behind ⇒ whole tile dropped new: clip at the plane, paint the surviving fragment

2. Why not just clamp z?

A tempting one-liner is to force every vertex to z = max(z, epsilon) and project anyway. It fails geometrically: a vertex at z = −50 clamped to z = 0.01 projects to a screen coordinate thousands of pixels away, in the wrong direction — the sign flip of the division mirrors it through the camera. The polygon smears into giant streaks across the frame instead of ending cleanly at the screen edge.

Clipping produces the geometrically correct cut: the polygon's new edge lies exactly on the near plane, and everything the rasterizer receives has z > 0.

3. How the clipping works

Clipping happens in camera space, after the transform stack has moved vertices relative to the viewer but before the perspective divide. The vertex loop is walked edge by edge (Sutherland-Hodgman style) against the plane z = nearPlaneDistance:

  1. An in-front vertex passes through unchanged.
  2. An edge that crosses the plane spawns a new vertex at the intersection, with position, UV and normal all interpolated with the same parameter t.
  3. A behind-plane vertex is skipped.
behind (z ≤ near) in front (z > near) near plane v0 v1 v2 p′ p″ t = 0.5 along v0 → v2 t = (near − z1) / (z2 − z1) p = p1 + t·(p2 − p1) uv = uv1 + t·(uv2 − uv1) every edge crossing the plane spawns an interpolated vertex; in-front vertices pass through unchanged

Interpolating UVs linearly along the 3D edge is exactly right for the perspective-correct texture mapper: the intersection vertex is a real point on the original edge, so its texture coordinate is the same blend of the endpoints' UVs. Textured fragments therefore show the correct texels right up to the cut, with no seam.

Only a polygon with all vertices behind the plane is culled — the legitimate version of the old behavior.

4. From clipped loop to pixels

A convex N-gon crossing the plane clips to a single contiguous loop of at most N+1 vertices. For the triangle-based rasterizers this means a triangle can become a quad, which is painted as a two-triangle fan sharing the first vertex — exact, because the clip of a convex polygon stays convex:

v0 v1 p″ p′ T1 = (v0, v1, p″) T2 = (v0, p″, p′) a triangle cut once becomes a quad; the rasterizer paints it as a 2-triangle fan sharing v0

Shape support:

Shape Behavior when straddling
SolidPolygon Clipped loop painted as triangle fan
TexturedTriangle Fan-painted with interpolated UVs (also inherited by lightmapped GI fragments)
Line Shortened to the in-front endpoint + intersection
Billboard Single anchor point: culled when behind, as before

Implementation notes:

  • Clipped output is stored per pipeline slot on the shape (clippedVertices(ctx)), so the triple-buffered pipeline can transform frame N+1 while frame N is still painting.
  • Depth sorting and tile binning use the clipped vertices' average Z and screen bounds — a clipped tile sorts as the fragment it became, not as the polygon that reached behind you.
  • New intersection vertices exist only in camera space; they are projected directly via Vertex.setCameraSpaceCoordinate(), bypassing the transform stack.

5. Configuration

The near plane distance is a per-context knob, in world units:

// Default is 1.0; smaller values let the camera press closer to
// geometry before the scissors bite, at the cost of larger projected
// coordinates for clipped fragments.
viewPanel.getRenderingContext().nearPlaneDistance = 0.5;

Created: 2026-09-08 ti 00:46

Validate